{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## <center>Открытый курс по машинному обучению</center>\n",
    "<center>Автор материала: Максим Самсонов (@mvsamsonov)</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Подробно о CountVectorizer, TfIdfVectorizer и базовой обработке текстов"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Алгоритмы машинного обучения работают с числовыми и категориальными данными. Если же у нас текст, его надо как-то привести к числовому виду. Для этого текст проходит предобработку, в которой можно выделить следующие стадии:\n",
    "\n",
    "1. <b>Токенизация</b>: выделение из текста слов (токенов), например с помощью регулярных выражений.\n",
    "2. <b>Отсечение стоп-слов</b>: слова, которые встречаются слишком часто, не несут никакой информации, только зашумляют данные, и их можно удалить. Это можно сделать с помощью словаря стоп-слов или просто отсекая слова, которые встречаются в обрабатываемых текстах чаще всего.\n",
    "3. <b>Стемминг/лемматизация</b>: приведение слов к начальной форме, чтобы игрорировать различия во временах, числах, падежах и прочее. При стемминге используется алгоритм, отсекающий суффиксы (который, например, преобразует books в book, leaves в leav, а was в wa), при лемматизации - словарь, который позволит выполнить эту работу более точно (но за большее время, и, собственно, нужен словарь).\n",
    "4. <b>Векторизация</b>: собственно то, ради чего всё затевалось, преобразование набора слов в набор чисел. Двумя простейшими подходами являются bag-of-words, при котором просто считается, как часто встретилось каждое слово, и TF-IDF (term frequency - inverse document frequency), при котором больший вес даётся тем словам, которые встречаются в обрабатываемых текстах более редко.\n",
    "\n",
    "Второй и третий этапы можно пропускать."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Токенизация\n",
    "Токенизация часто делается автоматически в рамках векторизации. Но рассмотрим, как её можно делать отдельно. Если бы тексты не содержали пунктуации, можно было бы просто воспользоваться методом split, но с пунтуацией всё чуть сложнее. Воспользуемся проверенной временем библиотекой NLTK."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import nltk\n",
    "\n",
    "nltk.download('punkt')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tokenized = nltk.word_tokenize(\"It's a picture (painting) of goddess Ma'at\")\n",
    "tokenized"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как мы видим, NLTK рассматривает пунктуацию как отдельные токены (что в каких-то случаях может быть полезно). Интересно, что имя с апострофом правильно распозналось как одно слово, а \"it's\" корректно разбилось на два токена."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Стоп-слова\n",
    "Произведём отсечение стоп-слов по словарю."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stopwords = nltk.corpus.stopwords.words('english')\n",
    "stopwords[:10]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "[w for w in tokenized if w.lower() not in stopwords]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Отфильтровались местоимение, артикль и предлог."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Стемминг\n",
    "Стемминг просто отсекает то, что посчитает изменяемым окончанием слова.\n",
    "\n",
    "В качестве примера рассмотрим стеммер Портера (алгоритм, предложенный Мартином Портером ещё в 1979 году)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stemmer = nltk.stem.PorterStemmer()\n",
    "[stemmer.stem(w) for w in [\"plays\", \"played\", \"playing\"]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видим, разные формы слова успешно приводятся к начальной форме. Применим стеммер к тексту."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "text = \"When you have eliminated all which is impossible, then whatever remains, however improbable, must be the truth.\"\n",
    "[stemmer.stem(w) for w in nltk.word_tokenize(text)]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как мы видим, стеммер просто откидывает суффиксы. То, что получается в результате, не всегда является словом. Но раз наша цель привести текст к числам, то почему бы и нет. Конечно, при этом разные слова могут вдруг оказаться одинаковыми. Будет ли от стемминга больше вреда или пользы - зависит от задачи.\n",
    "\n",
    "Стеммер Портера является одним из первых и простейших алгоритмов стемминга. Есть и другие, например, Snowball."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Лемматизация\n",
    "В отличие от стемминга, при лемматизации происходит не откидывание суффикса, а приведение слова к начальной форме с помощью словаря. Рассмотрим пример."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from nltk.stem import WordNetLemmatizer\n",
    "\n",
    "lemm = WordNetLemmatizer()\n",
    "[lemm.lemmatize(w) for w in ['wolves', 'women', 'eliminated', 'went']]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видим, данный lemmatizer отлично справляется с существительными, но не справляется с глаголами. Это происходит потому что он по умолчанию считает всё существительными. Если явно подсказать ему, что это глагол, то он справится тоже:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "[lemm.lemmatize(w, pos='v') for w in ['eliminated', 'went']]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Bag-of-words (CountVectorizer)\n",
    "Рассмотрим теперь самый простой способ приведения текста к набору чисел. Для каждого слова посчитаем, как часто оно встречается в тексте. Результаты запишем в таблицу. Строки будут представлять тексты, столбцы -- слова. Если на пересечении строки с столбца стоит число 5, значит данное слово встретилось в данном тексте 5 раз. В большинстве ячеек будут нули. Поэтому хранить это всё удобнее в виде разреженных матриц (т.е. хранить только ненулевые значения).\n",
    "\n",
    "Таким образом, при построении \"мешка слов\" можно выделить следующие действия:\n",
    "1. Токенизация.\n",
    "2. Построение словаря: собираем все слова, которые встречались в текстах и пронумеровываем их (по алфавиту, например).\n",
    "3. Построение разреженной матрицы.\n",
    "\n",
    "В sklearn алгоритм приведения текста в bag-of-words реализован в виде класса CountVectorizer. Рассмотрим пример."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.feature_extraction.text import CountVectorizer\n",
    "\n",
    "count_vectorizer = CountVectorizer()\n",
    "texts = [\"It is a capital mistake to theorize before one has data.\",\n",
    "         \"One begins to twist facts to suit theories, instead of theories to suit facts.\"]\n",
    "\n",
    "bow = count_vectorizer.fit_transform(texts)\n",
    "bow.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Результат содержит 2 строки (для 2 текстов) и 18 столбцов (для 18 разных слов). Посмотрим словарь:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "count_vectorizer.vocabulary_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видим, ни стемминга, ни лемматизации по умолчанию не производится.\n",
    "\n",
    "Результат преобразования:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bow.todense()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Т.о. \"before\" только в первом тексте, \"begins\" только во втором, (...) , \"to\" один раз в первом и 3 раза во втором, \"twist\" только во втором.\n",
    "\n",
    "### Стоп-слова и другие методы отсечения лишнего\n",
    "\n",
    "#### Стоп-слова\n",
    "Можно легко включить отсечение стоп-слов:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "count_vectorizer = CountVectorizer(stop_words='english')\n",
    "bow = count_vectorizer.fit_transform(texts)\n",
    "count_vectorizer.vocabulary_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bow.todense()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Параметр max_df\n",
    "\n",
    "Помимо стоп-слов есть и другие способы отсечения лишнего. Например, можно откидывать слова, которые встречаются слишком часто, с помощью параметра max_df. Установив max_df=2 мы откинем, все слова, которые встречаются более, чем в 2 документах."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "texts = [\"Soft kitty lyrics:\", \"Soft kitty, warm kitty\", \"Little ball of fur\", \"Happy kitty, sleepy kitty\", \"Purr, purr, purr\"] \n",
    "count_vectorizer = CountVectorizer(max_df=2)\n",
    "bow = count_vectorizer.fit_transform(texts)\n",
    "count_vectorizer.vocabulary_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видим, в словаре нет \"kitty\", т.к. оно встречается аж в 3 текстах, а мы поставили max_df=2.\n",
    "\n",
    "max_df может быть вещественным числом, тогда оно интерпретируется как доля документов.\n",
    "\n",
    "#### Параметр min_df\n",
    "С другой стороны, если слово встречается очень редко, оно скорее всего тоже не представляет интереса. Такие слова можно откидывать с помощью параметра min_df:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "count_vectorizer = CountVectorizer(min_df=2)\n",
    "bow = count_vectorizer.fit_transform(texts)\n",
    "count_vectorizer.vocabulary_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видим, в словаре остались только слова, которые встретились не менее, чем в 2 документах."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Token pattern\n",
    "Построим bag of words для простого предложения и посмотрим словарь."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "count_vectorizer = CountVectorizer()\n",
    "bow = count_vectorizer.fit_transform([\"I am a cat\"])\n",
    "count_vectorizer.vocabulary_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Что-то не так. Слов было 4, а тут только 2.\n",
    "\n",
    "По умолчанию CountVectorizer считает словами только слова длины не менее 2. Для того чтобы это изменить, используется параметр token_pattern. По умолчанию он равен регулярному выражению '(?u)\\b\\w\\w+\\b' (\\b обозначает границы слов, \\w обозначает букву, \\w+ - непустую последовательность букв). Значит, удалив первую \\w, получим то, что нужно:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "count_vectorizer = CountVectorizer(token_pattern=r'(?u)\\b\\w+\\b')\n",
    "bow = count_vectorizer.fit_transform([\"I am a cat\"])\n",
    "count_vectorizer.vocabulary_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Биграммы, триграммы, n-граммы\n",
    "По умолчанию bag-of-words (как следует из названия) представляет собой просто мешок слов. То есть для него предложения \"It's not good, it's bad!\" и \"It's not bad, it's good!\" абсолютно эквивалентны. Понятно, что при этом теряется много информации. Можно рассматривать не только отдельные слова, а последовательности длиной из 2 слов (биграммы), из 3 слов (триграммы) или в общем случае из n слов (n-граммы). На практике обычно задаётся диапазон от 1 до n.\n",
    "\n",
    "Рассмотрим пример:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "texts = [\"Soft kitty lyrics:\", \"Soft kitty, warm kitty\", \"Little ball of fur\", \"Happy kitty, sleepy kitty\", \"Purr, purr, purr\"]\n",
    "count_vectorizer = CountVectorizer(ngram_range=(1,2))\n",
    "bow = count_vectorizer.fit_transform(texts)\n",
    "count_vectorizer.vocabulary_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На практике вряд ли есть большой смысл выделять последовательности более, чем из 5 слов, но n-граммы для n равного 3 или 4 вполне полезны."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ограничение количества признаков\n",
    "Понятно, что с ростом n количество выделенных n-грамм быстро растёт. Для ограничения количества признаков можно использовать параметр max_features. В этом случае будет создано не более max_features признаков (будут выбраны самые часто встречающиеся слова и последовательности слов). Например:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "count_vectorizer = CountVectorizer(ngram_range=(1,3), max_features=10)\n",
    "bow = count_vectorizer.fit_transform(texts)\n",
    "count_vectorizer.vocabulary_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "n-грамы выбираются из интервала от 1 до 3, из было бы больше 30, но мы оставляем только 10 самых важных."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Слова или символы\n",
    "До сих пор мы в качестве элементов текста рассматривали слова. Но иногда бывает полезно рассматривать текст как последовательность отдельных букв. Если мы объединим эту идею с n-граммов, то получится, что нас интересует, насколько часто в тексте встречаются отдельные буквы, сочетания из двух букв, трёх букв, и т.д.\n",
    "\n",
    "Чтобы переключиться \"в режим отдельных букв\" используется параметр analyzer='char'. Очевидно, что количество вариантов будет относительно большим даже для небольшого текста, поэтому тут особенно важно не забыть ограничить max_features."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "count_vectorizer = CountVectorizer(ngram_range=(1,6), analyzer='char', max_features=10)\n",
    "bow = count_vectorizer.fit_transform(texts)\n",
    "count_vectorizer.vocabulary_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Несмотря на то, что разрешалось использовать последовательности длиной от 1 до 6, длина самых частых оказалась от 1 до 3."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Слова И символы\n",
    "На практике бывает полезно попробовать рассматривать как слова, так и буквы. Или даже использовать оба варианта вместе: построить представления на основе букв, на основе слов и объединить их с помощью hstack."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## TF-IDF\n",
    "У подхода bag-of-words есть существенный недостаток. Если слово встречается 5 раз в конкретном документе, но и в других документах тоже встречается часто, то его наличие в документе не особо-то о чём-то говорит. Если же слово 5 раз встречается в конкретном документе, но в других документах встречается редко, то его наличие (да ещё и многократное) позволяет хорошо отличать этот документ от других. Однако с точки зрения bag-of-words различий не будет: в обеих ячейках будет просто число 5.\n",
    "\n",
    "Отчасти это решается исключением стоп-слов (и слишком часто встречающихся слов), но лишь отчасти. Другой идеей является отмасштабировать получившуюся таблицу с учётом \"редкости\" слова в наборе документов (т.е. с учётом информативности слова).\n",
    "\n",
    "\\begin{equation*}\n",
    "tfidf = tf * idf \\\\\n",
    "idf = log \\frac {N + 1}{N_w + 1} + 1\n",
    "\\end{equation*}\n",
    "\n",
    "Здесь tf это частота слова в тексте (то же самое, что в bag of words), N - общее число документов, Nw - число документов, содержащих данное слово.\n",
    "\n",
    "То есть для каждого слова считается отношение общего количества документов к количеству документов, содержащих данное слово (для частых слов оно будет ближе к 1, для редких слов оно будет стремиться к числу, равному количеству документов), и на логарифм от этого числа умножается исходное значение bag-of-words (к числителю и знаменателю прибавляется единичка, чтобы не делить на 0, и к логарифму тоже прибавляется единичка, но это уже технические детали). После этого в sklearn ещё проводится L2-нормализация каждой строки.\n",
    "\n",
    "В sklearn есть два класса для поддержки TF-IDF: TfidfVectorizer и TfidfTransformer, рассмотрим их."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### TfidfVectorizer\n",
    "Этот класс применяется аналогично CountVectorizer:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.feature_extraction.text import TfidfVectorizer\n",
    "\n",
    "texts = [\"Soft kitty lyrics:\", \"Soft kitty, warm kitty\", \"Little ball of fur\", \"Happy kitty, sleepy kitty\", \"Purr, purr, purr\"] \n",
    "tfidf_vectorizer = TfidfVectorizer()\n",
    "tfidf = tfidf_vectorizer.fit_transform(texts)\n",
    "tfidf_vectorizer.vocabulary_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Словарь содержит те же 10 значений, которые были бы и для CountVectorizer. Но значения в таблице другие:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tfidf.todense()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "сравним результат с результатом работы CountVectorizer:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "CountVectorizer().fit_transform(texts).todense()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ненулевые значения находятся на тех же местах, но отмасштабированы в зависимости от частоты слов.\n",
    "\n",
    "Рассмотрим для примера первую строку. В ней три ненулевых значения, с индексами 3, 5 и 9 (kitty, lyrics и soft). Все три слова встречаются в тексте по одному разу, поэтому в bag-of-words для каждого из них стоит значение 1. Но kitty встречается ещё в двух документах, поэтому tfidf для неё лишь 0.46; слово lyrics встречается только в этом документе, поэтому у него значение выше: 0.69; soft встречается ещё в одном документе, у него 0.56 (меньше 0.69, но больше 0.46)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Параметр sublinear_tf\n",
    "Большая часть параметров у CountVectorizer и TfidfVectorizer одинакова. Но у TfidfVectorizer есть один важный дополнительный параметр.\n",
    "\n",
    "Как видно из формулы tfidf = tf * idf, если слово будет встречаться не один, а два раза, то tfidf вырастет в два раза. Если слово будет встречаться не один, а 10 раз, то tfidf вырастет в 10 раз. На итоговой строке, конечно, производится нормализация, так что значение всё равно останется в пределах единицы, но за счёт других значений в этой строке. В качестве примера добавим в первую строку ещё пару слов lyrics:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "texts = [\"Soft kitty lyrics lyrics lyrics:\", \n",
    "         \"Soft kitty, warm kitty\", \"Little ball of fur\", \"Happy kitty, sleepy kitty\", \"Purr, purr, purr\"] \n",
    "\n",
    "TfidfVectorizer().fit_transform(texts).todense()[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Значение tfidf выросло с 0.69015927 до 0.94400181, а остальные два упали почти в 2 раза.\n",
    "\n",
    "Вопрос - хотим ли мы таких драматических изменений. Если не хотим, то можно использовать параметр sublinear_tf=True. При его использовании вместо tf будет браться 1 + log(tf). То есть по-прежнему с ростом tf будет расти и tfidf, но уже не так радикально (и соответственно остальные значения будут уменьшаться не так быстро):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "TfidfVectorizer(sublinear_tf=True).fit_transform(texts).todense()[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для некоторых задач это может дать прирост в качестве."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### TfidfTransformer\n",
    "Как мы видели, tfidf строится на основе tf, т.е. bag-of-words. Что если у нас уже есть готовый bag-of-words, обязательно ли строить tfidf на сыром тексте или воспользоваться готовым промежуточным результатом?\n",
    "\n",
    "TfidfTransformer строит tfidf на основе результата работы CountVectorizer. В качестве примера построим CountVectorizer:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "texts = [\"Soft kitty lyrics:\", \"Soft kitty, warm kitty\", \"Little ball of fur\", \"Happy kitty, sleepy kitty\", \"Purr, purr, purr\"] \n",
    "count_vectorizer = CountVectorizer()\n",
    "bow = count_vectorizer.fit_transform(texts)\n",
    "bow.todense()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "и применим преобразование:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.feature_extraction.text import TfidfTransformer\n",
    "\n",
    "tfidf = TfidfTransformer().fit_transform(bow)\n",
    "tfidf.todense()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Получилась та же матрица, что и при использовании TfidfVectorizer напрямую.\n",
    "\n",
    "Параметры, отвечающие за построение tf, настраиваются в CountVectorizer; а параметры, специфичные для TF-IDF (такие как sublinear_tf), настраиваются уже в TfidfTransformer."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Краткие итоги\n",
    "\n",
    "* Для работы с текстами надо как-то преобразовывать их к числовому представлению\n",
    "* Классическими и самыми простыми средствами для этого являются bag-of-words и tf-idf\n",
    "* В sklearn есть удобные классы-векторайзеры, реализующие bag-of-words и tf-idf (и трансформер для преобразования от bag-of-words к tf-idf)\n",
    "* Они реализуют токенизацию, векторизацию и при желании отсечение стоп-слов\n",
    "* У векторайзеров много параметров, позволяющих добиваться наиболее удачного представления текста. Знание этих параметров (и понимание того, как они работают) может сильно повысить эффективность обучения и качество результата\n",
    "* Отдельно токенизацию, стемминг и прочее можно делать, например, средствами NLTK"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
